#!/bin/bash

# Copyright (c) 2022 - 2023 by Apex.AI Inc. All rights reserved.
# Copyright (c) 2023 - 2024 by ekxide IO GmbH. All rights reserved.
#
# This program and the accompanying materials are made available under the
# terms of the Apache Software License 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0, or the MIT license
# which is available at https://opensource.org/licenses/MIT.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT

# This script checks code files with clang-tidy
# Example usage: ./tools/scripts/clang_tidy_check.sh full|hook|ci_pull_request

set -e

COLOR_OFF='\033[0m'
COLOR_RED='\033[1;31m'
COLOR_GREEN='\033[1;32m'
COLOR_YELLOW='\033[1;33m'

MODE=${1:-full} # Can be either `full` for all files or `hook` for formatting with git hooks

FILE_FILTER="\.(h|hpp|inl|c|cpp)$"

fail() {
    printf "${COLOR_RED}error: %s: %s${COLOR_OFF}\n" ${FUNCNAME[1]} "${1:-"Unknown error"}"
    exit 1
}

CLANG_TIDY_VERSION=18
CLANG_TIDY_CMD="clang-tidy-$CLANG_TIDY_VERSION"
if ! command -v $CLANG_TIDY_CMD &> /dev/null
then
    CLANG_TIDY_MAJOR_VERSION=$(clang-tidy --version | sed -rn 's/.*([0-9][0-9])\.[0-9].*/\1/p')
    if [[ $CLANG_TIDY_MAJOR_VERSION -lt "$CLANG_TIDY_VERSION" ]]; then
        echo "Warning: clang-tidy version $CLANG_TIDY_VERSION or higher is not installed."
        echo "This may cause undetected warnings or that warnings suppressed by NOLINTBEGIN/NOLINTEND will not be suppressed."
    fi
    CLANG_TIDY_CMD="clang-tidy"
fi


WORKSPACE=$(git rev-parse --show-toplevel)
cd "${WORKSPACE}"

# we have to ensure that everything is build otherwise clang-tidy may not check every file
if ! [[ -f build/build_is_clang_tidy_check_generated ]]; then
    echo "creating new build directory since the existing one was not generated by this script"
    rm -rf build
    export CXX=clang++
    export CC=clang
    cmake -Bbuild -Hiceoryx_meta -DBUILD_ALL=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
    touch build/build_is_clang_tidy_check_generated
fi

echo "Using clang-tidy version: $($CLANG_TIDY_CMD --version | sed -n "s/.*version \([0-9.]*\)/\1/p" )"

noSpaceInSuppressions=$(git ls-files | grep -E "$FILE_FILTER" | xargs -I {} grep -h '// NOLINTNEXTLINE (' {} || true)
if [[ -n "$noSpaceInSuppressions" ]]; then
    echo -e "${COLOR_RED}Remove space between NOLINTNEXTLINE and '('!${COLOR_OFF}"
    echo "$noSpaceInSuppressions"
    exit 1
fi

function scanWithFileList() {
    FILE_WITH_SCAN_LIST=$1
    FILES_TO_SCAN=$2

    if ! test -f "$FILE_WITH_SCAN_LIST"
    then
        echo -e "${COLOR_RED}Scan list file '${FILE_WITH_SCAN_LIST}' does not exist! Aborting!${COLOR_OFF}"
        return 1
    fi

    SEPARATOR=''
    while IFS= read -r LINE
    do
        # add files until the comment section starts
        if [[ "$(echo $LINE | grep "#" | wc -l)" == "1" ]]; then
            break
        fi
        SCAN_LIST_ENTRIES+="${SEPARATOR}${LINE}"
        SEPARATOR=$'\n'
    done < "$FILE_WITH_SCAN_LIST"

    if [[ -z $SCAN_LIST_ENTRIES ]]
    then
        echo -e "${COLOR_YELLOW}'${FILE_WITH_SCAN_LIST}' is empty skipping scan!${COLOR_OFF}"
        return 0
    fi

    ALL_FILES_FROM_SCAN_LIST=$(find ${SCAN_LIST_ENTRIES} -type f | grep -E ${FILE_FILTER} | sort | uniq)

    FILES_TO_SCAN_ARRAY=(${FILES_TO_SCAN})
    NUMBER_OF_FILES=${#FILES_TO_SCAN_ARRAY[@]}
    if [[ ${NUMBER_OF_FILES} -gt 0 ]]
    then
        FILES=""
        SEPARATOR=''
        SKIP_MESSAGE_PRINTED=0
        for FILE in ${FILES_TO_SCAN}; do
            if $(echo ${ALL_FILES_FROM_SCAN_LIST} | grep -q ${FILE})
            then
                FILES+="${SEPARATOR}${FILE}"
                SEPARATOR=$'\n'
            else
                if [[ ${SKIP_MESSAGE_PRINTED} -eq 0 ]]; then
                    echo -e "${COLOR_YELLOW}Skipping files which are not part of '${FILE_WITH_SCAN_LIST}' ...${COLOR_OFF}"
                    SKIP_MESSAGE_PRINTED=1
                fi
                echo -e "${COLOR_YELLOW}[#]${COLOR_OFF} ${FILE}"
            fi
        done

        if [[ ${SKIP_MESSAGE_PRINTED} -eq 1 ]]; then
            echo -e "${COLOR_YELLOW}... end of list with skipped files!${COLOR_OFF}"
        fi
        scan "error" "$FILES"
    else
        echo "Performing full scan of all files in '${FILE_WITH_SCAN_LIST}'"
        scan "error" "${ALL_FILES_FROM_SCAN_LIST}"
    fi
}

function scan() {
    WARN_MODE=$1
    FILES=$2
    FILES_ARRAY=(${FILES})
    NUMBER_OF_FILES=${#FILES_ARRAY[@]}

    if [[ $WARN_MODE == "warn" ]]; then
        WARN_MODE_PARAM=""
    elif [[ $WARN_MODE == "error" ]]; then
        WARN_MODE_PARAM="--warnings-as-errors=*"
    else
        echo "Invalid parameter! Must be either 'warn' or 'error' but got '${WARN_MODE}'"
        return 1
    fi

    if [[ ${NUMBER_OF_FILES} -eq 0 ]]; then
        echo -e "${COLOR_YELLOW}-> nothing to do${COLOR_OFF}"
        return 0
    fi

    echo -e "${COLOR_GREEN}Processing files ...${COLOR_OFF}"
    MAX_CONCURRENT_EXECUTIONS=$(nproc)
    CURRENT_CONCURRENT_EXECUTIONS=0
    echo "Concurrency set to '${MAX_CONCURRENT_EXECUTIONS}'"
    FILE_COUNTER=1
    for FILE in $FILES; do
        # run multiple clang-tidy instances concurrently
        if [[ ${CURRENT_CONCURRENT_EXECUTIONS} -ge ${MAX_CONCURRENT_EXECUTIONS} ]]; then
            wait -n # wait for one of the background processes to finish
            CURRENT_CONCURRENT_EXECUTIONS=$((CURRENT_CONCURRENT_EXECUTIONS - 1))
        fi

        echo -e "${COLOR_GREEN}[${FILE_COUNTER}/${NUMBER_OF_FILES}]${COLOR_OFF} ${FILE}"
        FILE_COUNTER=$((FILE_COUNTER + 1))

        if test -f "$FILE"; then
            SECONDS_START=${SECONDS}
            $(${CLANG_TIDY_CMD} ${WARN_MODE_PARAM} --quiet -p build ${FILE} ${EXTRA_ARG} >&2 \
            || exit $? \
            && echo echo -e "${COLOR_YELLOW} $((${SECONDS}-${SECONDS_START}))s${COLOR_OFF} to scan '${FILE}'") &
            CURRENT_CONCURRENT_EXECUTIONS=$((CURRENT_CONCURRENT_EXECUTIONS + 1))
        else
            echo -e "${COLOR_RED}File does not exist! Aborting!${COLOR_OFF}"
            return 1
        fi
    done
    # wait on each background process individually to abort script when a process exits with an error
    while [[ ${CURRENT_CONCURRENT_EXECUTIONS} -ne 0 ]]; do
        wait -n # wait for one of the background processes to finish
        CURRENT_CONCURRENT_EXECUTIONS=$((CURRENT_CONCURRENT_EXECUTIONS - 1))
    done

    echo -e "${COLOR_GREEN}... done!${COLOR_OFF}"
}

if [[ "$MODE" == "hook"* ]]; then
    if [[ $2 ]]; then
        FILE_WITH_SCAN_LIST=$2
    fi

    MODIFIED_FILES=$(git diff --cached --name-only --diff-filter=CMRT | grep -E "$FILE_FILTER" | cat)
    MODIFIED_FILES_ARRAY=($MODIFIED_FILES)
    NUMBER_OF_MODIFIED_FILES=${#MODIFIED_FILES_ARRAY[@]}
    echo ""
    echo "Checking modified files with Clang-Tidy"
    if [[ $FILE_WITH_SCAN_LIST && ${NUMBER_OF_MODIFIED_FILES} -gt 0 ]]; then
        scanWithFileList "${FILE_WITH_SCAN_LIST}" "${MODIFIED_FILES}"
    else
        scan "warn" "${MODIFIED_FILES}"
    fi

    # List only added files
    ADDED_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E "$FILE_FILTER" | cat)
    echo ""
    echo "Checking added files with Clang-Tidy"
    scan "error" "${ADDED_FILES}"

    exit
elif [[ "$MODE" == "full"* ]]; then
    DIRECTORY_TO_SCAN=$2

    if [[ -n $DIRECTORY_TO_SCAN ]]
    then
        if ! test -d "$DIRECTORY_TO_SCAN"
        then
            echo "${COLOR_RED}The directory which should be scanned '${DIRECTORY_TO_SCAN}' does not exist! Aborting!${COLOR_OFF}"
            exit 1
        fi

        echo ""
        echo "Scanning all files in '${DIRECTORY_TO_SCAN}'"
        FILES=$(find $DIRECTORY_TO_SCAN -type f | grep -E $FILE_FILTER )
        scan "warn" "${FILES}"
        exit $?
    else
        FILES=$(find iceoryx_* tools/introspection -type f | grep -E "$FILE_FILTER")
        echo ""
        echo "Checking all files with Clang-Tidy"
        scan "warn" "${FILES}"
        exit $?
    fi
elif [[ "$MODE" == "scan_list"* ]]; then
    FILE_WITH_SCAN_LIST=$2
    FILES_TO_SCAN=$3 # if there is more than one file, they must be enclosed in quotes -> "file1 file2 file3"

    scanWithFileList "${FILE_WITH_SCAN_LIST}" "${FILES_TO_SCAN}"

    exit $?
else
    echo "Invalid mode: ${MODE}"
    exit 1
fi
